package de.tuberlin.dima.dkey.client;


import java.io.File;
import java.util.HashMap;
import java.util.LinkedList;
import org.apache.log4j.Logger;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import de.tuberlin.dima.dkey.util.GConfig;
import de.tuberlin.dima.dkey.util.GEdge;
import de.tuberlin.dima.dkey.util.GraphNotSameException;
import de.tuberlin.dima.dkey.util.GraphViz;

import de.tuberlin.dima.dkey.nodes.KeyNode;
import de.tuberlin.dima.dkey.nodes.MultiValueNode;
import de.tuberlin.dima.dkey.nodes.TextNode;
import de.tuberlin.dima.dkey.nodes.WordNode;
import de.tuberlin.dima.dkey.nodes.GNode;

public class GTree{

	static Logger logger = Logger.getLogger(GTree.class);

	private LinkedList<GNode> nodeList = new LinkedList<GNode>();

	private LinkedList<GEdge> edgeList = new LinkedList<GEdge>();

	int idcounter = 0;

	private GConfig config = null;
	
	private GNode root = null;
	
	
	public GTree(){
		config = new GConfig();
		//For now this will be ok
		config.setRemoveCapitalisation(true);
	}
	
	public GTree(String treeJson) throws Exception{
		
		if(treeJson == null){
			throw new Exception("You need to add something into the String");
		}

		
		JsonParser parser = new JsonParser();
		JsonArray array;

		try{
			array = parser.parse(treeJson).getAsJsonObject().get("graphs").getAsJsonArray();
		}catch(IllegalStateException e){
			logger.error("JSon Parser Error: " + e);
			return;
		}
		
		for (int i =0 ; i < array.size();i++){
			//Looping through the graphs now

			JsonObject graph = array.get(i).getAsJsonObject();
			
			HashMap<Integer, GNode> tmpList = new HashMap<Integer, GNode>();
			for (JsonElement node : graph.get("nodes").getAsJsonArray()){
				GNode tmpNode;
			
				@SuppressWarnings("rawtypes")
				Class cls =  Class.forName("de.tuberlin.dima.dkey.nodes." + node.getAsJsonObject().get("type").getAsString());
				tmpNode = (GNode) cls.getConstructors()[1].newInstance(graph.get("id").getAsInt(), node.getAsJsonObject().get("id").getAsInt(), node.getAsJsonObject().get("value").getAsString());


				this.addBaseNode(tmpNode);
				
				//We need this for the edges
				tmpList.put(node.getAsJsonObject().get("id").getAsInt(), tmpNode);

			}
			
			//System.out.println(graph.get("edges").getAsJsonArray() );
			for (JsonElement edge : graph.get("edges").getAsJsonArray()){
					this.addBaseEdge(tmpList.get(edge.getAsJsonObject().get("source").getAsInt()), tmpList.get(edge.getAsJsonObject().get("sink").getAsInt()));
			}
			
		}

	}
	
	public GNode addWordNode(String value) {
		if(value == null)
			return null;
		
		GNode node = new WordNode(-1, idcounter++, value);
		return addBaseNode(node);
	}
	
	public GNode addTextNode(String value) {
		if(value == null)
			return null;
		
		GNode node = new TextNode(-1, idcounter++, value);
		return addBaseNode(node);
	}
	
	public GNode addKeyNode(String value) {
		if(value == null)
			return null;
		
		GNode node = new KeyNode(-1, idcounter++, value);
		return addBaseNode(node);
	}
	
	public GNode addMultiValueNode(String value) {
		if(value == null)
			return null;
		
		GNode node = new MultiValueNode(-1, idcounter++, value);
		return addBaseNode(node);
	}

	private GNode addBaseNode(GNode node) {
		nodeList.add(node);
		return node;
	}

	public GEdge addBaseEdge(GNode source, GNode sink) {
		GEdge edge = new GEdge(-1, source.getId(), sink.getId());
		edgeList.add(edge);
		return edge;
	}
	
	
	/**
	 * This finds the root node in the most simple way
	 * 
	 * It just looks for all the nodes and puts them in a list
	 * then it just checks all the incoming edges to the nodes
	 * 
	 * Root nodes are all nodes that have no incoming edges
	 * 
	 * @return a list of possible root nodes
	 */
	public LinkedList<GNode> getRootNodes(){
		//This is not very good and not very fast
		HashMap<Integer,Boolean> idList = new HashMap<Integer,Boolean>();

		for (GNode node : this.nodeList){
			if (!idList.containsKey(node.getId())){
				idList.put(node.getId(), true);
			}

		}

		for (GEdge edge : this.edgeList){
			if(idList.containsKey(edge.getSink())){
				idList.put(edge.getSink(), false) ;
			}
		}

		LinkedList<GNode> returnList = new LinkedList<GNode>();

		for(Integer i : idList.keySet()){
			if (idList.get(i) == true){
				returnList.add( searchForNodeInNodeList(nodeList, i) );
			}
		}

		//If there was no room make it crash and burn
		//Idiot user
		if(returnList.size() ==0 )
			return null;

		return returnList;
	}
	
	public GNode searchForNodeInNodeList(LinkedList<GNode> list, int id){
		for(GNode node : list){
			if(node.getId() == id)
				return node;
		}
		return null;
	}	
	
	/**
	 * If you want to you can manually set the root
	 * 
	 * This should not be used currently => Not fully tested
	 */
	protected void setRoot(GNode root){
		this.root = root;
	}
	
	
	/**
	 * Checks if the tree conforms to the std we set for our DB
	 * 
	 * !!! For now this might take for ever if you have a loop in your tree
	 * 
	 * @return true if the tree is valid; false otherwise
	 */
	public Boolean isValid(){
		LinkedList<GNode> rootnode = this.getRootNodes();
		
		if(rootnode == null){
			//rootnode can be null if there is no distinct root node
			return false;
		}
		
		//Needs one root
		if(this.root != null){
			//If there is a root set check that it is in the possible roots
			if (rootnode.contains(this.root)){
				return true;
			}
		}else{
			//We have no root set so there must be one distinct root
			if(rootnode.size() == 1 ){
				return true;
			}
		}
		
		//FIXME:
		//Only one connected tree
		//Can be traversed
		
		
		return false;
	}
	
	
	/**
	 * @return the JSON representation of the tree
	 */
	public String toString(){
		
		StringBuffer output = new StringBuffer();
		output.append("{ \"graphs\": [");
		//Save all nodes based on the Graphs
		Boolean firstRun = true;
			if (firstRun){firstRun = false;}else{output.append(",");}
			output.append("{");
			output.append("\"id\":\"" + this.hashCode() + "\"," );
			//Loop through all the graphs and save all the nodes
			output.append("\"nodes\": [" );
			firstRun = true;
			for (GNode node : this.nodeList){
				if (firstRun){firstRun = false;}else{output.append(",");}
				output.append("{");
				output.append("\"id\":\""+ node.getId() + "\"," );
				output.append("\"value\":\""+ node.getValue() + "\"," );
				output.append("\"type\":\""+ node.getType() + "\"" );	
				output.append("}");
			}
			output.append(" ]," );
			
			output.append("\"edges\": [" );
			firstRun = true;
			for (GEdge edge : this.edgeList){
				if (firstRun){firstRun = false;}else{output.append(",");}
				output.append("{");
				output.append("\"source\":\""+ edge.getSource() + "\"," );
				output.append("\"sink\":\""+ edge.getSink() + "\"" );
				output.append("}");				
			}
			output.append(" ]" );
			
			output.append("}");
			output.append("]");
			//We don't really need this as the id is the hashCode
			//output.append(",\"hashcode\":\""+this.hashCode()+"\"");
			output.append("}");
			
		return output.toString();
	}	

	/**
	 * A very simple hash implementation
	 * 
	 * Note that this is not very performant as it scans through the list and 
	 * every string etc ...
	 */
	public int hashCode(){
		
		int hash = this.edgeList.hashCode() * 32 + this.nodeList.hashCode();
		
		//We don't really want negative hash values and there are no unsigned ints
		if (hash < 0){
			hash = hash * -1;
		}
		return hash;
	}
	
	
	public LinkedList<GNode> getChildren( GNode node){
		return getChildren(nodeList, edgeList, node);
	}
	
	public LinkedList<GNode> getChildren(LinkedList<GNode>nodeList, LinkedList<GEdge> edgeList, GNode node){
		
		LinkedList<Integer> resultEdges = new LinkedList<Integer>();
		
		for (GEdge edge : edgeList){
			if (edge.getSource() == node.getId()){
				resultEdges.add(edge.getSink() );
			}
		}
		LinkedList<GNode> resultNodes = new LinkedList<GNode>();
		
		for(GNode snode : nodeList){
			if( resultEdges.contains(snode.getId()) ){
				resultNodes.add(snode);
			}
		}
		
		return resultNodes;
	}
	
	
	/**
	 * Prints a png of the tree
	 * Mainly for debug reasons
	 * 
	 * @param File Obj (please name the file .png otherwise there might be problems)
	 */
	public void paint(File out) {
		GraphViz gv = new GraphViz();
		gv.addln(gv.start_graph());
		for (GNode node : nodeList) {
			gv.addln("node" + node.getId() + "  [color="+ node.getColour() +" label=\"" + node.getId() +":"+ node.getValue() + " \"];");
		}

		for (GEdge edge : edgeList) {
			gv.addln("node" + edge.getSource() + "-> node" + edge.getSink());
		}
		gv.addln("}");

		gv.addln(gv.end_graph());

		//System.out.println(gv.getDotSource());
		
		String type = "png";
		//new File(fileName + "." + type);
		gv.writeGraphToFile(gv.getGraph(gv.getDotSource(), type), out);

	}

}
